EchoDemo's Blogs

使用 Dockerfile 定制镜像

编写一个简单的Dockerfile文件

Dockerfile是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

这里以定制nginx镜像为例,我们使用Dockerfile来定制。在一个空白目录中,建立一个文本文件,并命名为Dockerfile:

$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile

其内容为:

FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

FROM指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx 镜像的容器,再进行修改一样,基础镜像是必须指定的。而 FROM 就是指定基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。

FROM <image>
FROM <image>:<tag>
FROM <image>:<digest>

RUN执行命令

RUN指令是用来执行命令行命令的。由于命令行的强大能力,RUN指令在定制镜像时是最常用的指令之一。其格式有两种:

1、 shell 格式:RUN <命令>

2、 exec 格式:RUN ["可执行文件", "参数1", "参数2"]

就像直接在命令行中输入的命令一样。刚才写的Dockerfile中的RUN指令就是shell格式。而exec格式这更像是函数调用中的格式。此命令只有在执行docker build 时才会执行,其他情况下不会执行。

由于Docker的镜像是分层结构,Dockerfile里面一个指令的操作就是一层。比如下面的操作,一条RUN命令包含了更新源缓存,安装openjdk,清理垃圾,这样的好处是最终这一层会很小,假设你分开写,三个命令三个RUN指令,但是只有第二条命令才是你想要的,那么第一条产生的缓存垃圾就无法删除掉。这也算是docker镜像优化的一部分。

RUN yum update \
&& yum install openjdk-8-jdk  -y \
&& yum clean all 

构建镜像

编写好刚才的Dockerfile文件之后,在Dockerfile文件所在目录执行:

$ docker build -t nginx:v3 .

在这里我们指定了最终镜像的名称“-t nginx:v3”,构建成功后,我们就可以通过docker images看到nginx:v3这个镜像。如果你注意到上面的docker build命令最后,还有一个.,.表示当前目录,而Dockerfile文件就在当前目录,所以,这个点是在指定上下文路径。上面我们使用 docker build命令进行镜像构建。其格式为:

docker build [选项] <上下文路径/URL/->

docker build 的工作原理

Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。

当我们进行镜像构建的时候,并非所有定制都会通过RUN指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指令、ADD指令等。而 docker build命令构建镜像,其实并非在本地构建,而是在服务端,也就是Docker引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?

这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build命令得知这个路径后,会将路径下的所有内容打包,然后上传给Docker引擎。这样Docker引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

实际上Dockerfile的文件名并不要求必须为Dockerfile,而且并不要求必须位于上下文目录中,比如可以用-f参数指定某个文件作为Dockerfile,当然,一般大家习惯性的会使用默认的文件名 Dockerfile,以及会将其置于镜像构建上下文目录中。

Dockerfile常用指令详解

COPY指令复制文件

COPY [--chown=<user>:<group>] <源路径>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]

和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。COPY 指令将从构建上下文目录中 的文件或者目录复制到新的一层的镜像内的 <目标路径> 位置。比如:

COPY package.json /usr/src/app/

其中源路径可以是多个,甚至可以是通配符,如:

COPY hom* /mydir/
COPY hom?.txt /mydir/

目标路径:可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

ADD 更高级的复制文件

ADD指令和COPY的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。如果<源路径>为一个tar压缩文件的话,压缩格式为gzip, bzip2以及xz的情况下,ADD指令将会自动解压缩这个压缩文件到 <目标路径> 去。

Docker官方建议,尽可能的使用COPY,因为COPY的语义很明确,就是复制文件而已,而ADD则包含了更复杂的功能,其行为也不一定很清晰。最适合使用ADD的场合,就是所提及的需要自动解压缩的场合。

因此在 COPY 和 ADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD。

CMD 容器启动命令

CMD指令的格式和RUN相似,也是两种格式:

shell 格式:CMD <命令>

exec 格式:CMD ["可执行文件", "参数1", "参数2"...]

注意:Docker不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD指令就是用于指定默认的容器主进程的启动命令的。在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 “,而不要使用单引号。

如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:

CMD echo $HOME

在实际执行中,会将其变更为:

CMD [ "sh", "-c", "echo $HOME"]

在这里我们不要把RUN和CMD搞混了。RUN是构件容器时就运行的命令以及提交运行结果。CMD是容器启动时执行的命令,在构件时并不运行,构件时仅仅指定了这个命令到底是个什么样子

VOLUME定义匿名卷

VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>

容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在Dockerfile中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

看这个例子:

VOLUME  /data

这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。

那么Dockerfile中的VOLUME指令跟docker run中的-v参数实现的是不是一样的功能呢?并不然,其实VOLUME指令只是起到声明容器中的目录作为匿名卷,但是并没有将匿名卷绑定到宿主机指定目录的功能。但是当我们生成镜像的Dockerfile中以Volume声明了匿名卷,并且我们以这个镜像run了一个容器的时候,docker会在安装目录下的指定目录下面生成一个目录来绑定容器的匿名卷(这个指定目录不同版本的docker会有所不同)。

也就是说当我们在Dockerfile中声明了匿名卷但是run的时候没有使用 -v绑定匿名卷的话,那么docker就会在/var/lib/docker/volumes这个目录下创建一个目录来绑定匿名卷。

EXPOSE 声明端口

EXPOSE <端口1> [<端口2>...]

EXPOSE指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。

在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射EXPOSE的端口。要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。

-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

WORKDIR指定工作目录

WORKDIR <工作目录路径>

使用WORKDIR指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR会帮你建立目录。可以理解为shell的cd,启动容器的时候使用的意思为 docker run 启动容器时,默认进入到目录是WORKDIR 指定的。

USER 指定当前用户

USER <用户名>[:<用户组>]

USER指令和WORKDIR相似,都是改变环境状态并影响以后的层。WORKDIR是改变工作目录,USER则是改变之后层的执行RUN, CMD以及ENTRYPOINT这类命令的身份。当然,和WORKDIR一样,USER只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。如:

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

ENV 设置环境变量

ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。如:

ENV NODE_VERSION 7.2.0

RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
  && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
  && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
  && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
  && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
  && ln -s /usr/local/bin/node /usr/local/bin/nodejs

HEALTHCHECK健康检查

HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12 引入的新指令。当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy。HEALTHCHECK 支持下列选项:

--interval=<间隔>:两次健康检查的间隔,默认为 30 秒;

--timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;

--retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。

和 CMD, ENTRYPOINT 一样,HEALTHCHECK 只可以出现一次,如果写了多个,只有最后一个生效。

假设我们有个镜像是个最简单的Web服务,我们希望增加健康检查来判断其Web服务是否在正常工作,我们可以用curl来帮助判断,其Dockerfile的 HEALTHCHECK可以这么写:

FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s CMD curl -fs http://localhost/ || exit 1

这里我们设置了每5秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过3秒没响应就视为失败,并且使用“curl -fs http://localhost/ || exit 1”作为健康检查命令。

🐶 您的支持将鼓励我继续创作 🐶
-------------本文结束感谢您的阅读-------------